<?php

namespace System\library\database\sql\driver;

use System\library\Benchmark;
use System\library\config\Config;
use System\library\config\SqlConfig;
use System\library\database\sql\Sql;
use System\library\database\sql\IDriver;
use System\library\utility\Validate;

/**
 * <b>Class</b> :        Mysqli<br>
 * <b>Fonction</b> :     MySQLi driver
 * <br>
 * @version              1.0
 * @author               Steuf <dreadlokeur@gmail.com>
 * @license              Licence publique générale GNU http://www.gnu.org/licenses/gpl.html
 * @copyright            Copyright 2011 - MidichloriansPHP and contributors
 * @package              System
 * @subpackage           library\database\sql\driver
 * 
 * @see \Mysqli
 * @see System\library\database\sql\IDriver
 */
class Mysqli extends \Mysqli implements IDriver {

    /**
     * The SqlConfig name of this instance
     * @var String
     */
    protected $configName = '';
    /**
     * Check if it have an active connection to the server
     * @var Boolean
     */
    protected $isConnected = false;
    /**
     * The current server configuration
     * @var \ExFramework\SqlServer
     */
    protected $serverConfiguration = null;
    /**
     * The query string to execute
     * @var String
     */
    protected $query = null;
    /**
     * The statement of query
     * @var Mysqli_STMT
     */
    protected $statement = null;
    /**
     * Set Mysqli type convertion from Sql class to Mysqli format
     * @var Array
     */
    protected $paramType = array(
        Sql::TYPE_INT => 'i',
        Sql::TYPE_FLOAT => 'd',
        Sql::TYPE_STRING => 's',
        Sql::TYPE_BLOB => 'b'
    );
    /**
     * Binded parameters for prepared query
     * @var Array
     */
    protected $params = array();
    /**
     * Named parameters order
     * @var Array
     */
    protected $namedParamOrder = array();
    /**
     * The parameter type on query
     * @var Constant
     */
    protected $bindParamType = null;
    /**
     * A Mysqli_Result with metadata of query
     * @var Mysqli_Result
     */
    protected $metaData = null;
    /**
     * Get fields details of a query
     * @var Array
     */
    protected $field = array();
    /**
     * Logs of Mysqli query
     * @var Array
     */
    protected $logs = array(
        array('SQL Statement', 'Parameters', 'Time', 'Error')
    );
    protected $explains = array();
    protected static $explain = null;

    /**
     * Construct object with the SqlConfig name
     * @param String $config
     * @return Void
     */
    public function __construct($config) {
        if (!is_string($config) || empty($config))
            throw new \Exception('Configuration must be an non empty string');
        $this->configName = $config;
        parent::init();
    }

    /**
     * Prepare a query to execute
     * @param String $query
     * @return Void
     */
    public function setQuery($query) {
        try {
            $this->connection(!$this->isReadQuery($query));
        } catch (\Exception $e) {
            throw new \Exception('Unable to connect to server with config "' . $this->configName . '"', null, $e);
        }

        // Close previous statement
        if ($this->haveStatement())
            $this->statement->close();
        $this->metaData = null;
        $this->params = array();
        $this->fields = array();
        $this->query = $query;
        // Check if it have named or positionnal parameter on query
        preg_match_all('#:([0-9a-zA-Z_-]+)#', $query, $namedParam);
        if (count($namedParam[1]) > 0) {
            if (strpos($this->query, '?') !== false)
                throw new \Exception('You cannot mixed positional and named parameter on query');
            $query = \preg_replace('#:([0-9a-zA-Z_-]+)#', '?', $query);
            // set param bind type to named
            $this->bindParamType = Sql::PARAM_NAMED;
        }else {
            // set param bind type to positionnal
            $this->bindParamType = Sql::PARAM_POSITIONAL;
        }
        $this->namedParamOrder = $namedParam[1];

        $this->statement = $this->prepare($query);
        if (!$this->statement)
            throw new \Exception('An error occured when preparing your query: ' . $this->error);
        return;
    }

    /**
     * Bind parameter to query
     * @param Mixed $parameter
     * @param Constant $type
     * @param String $key
     * @return Mysqli
     */
    public function bindParam($parameter, $type, $key = false) {
        // Check if type exist
        if (!\key_exists($type, $this->paramType))
            throw new \Exception('Type "' . $type . '" don\'t exist');

        // If key setted, check if it's variable normalization format
        if ($key !== false && !Validate::isVariableName($key))
            throw new \Exception('Key for param must bet start with letter and can have caracters : a-zA-Z0-9_-');

        // Search if is not mixed key format
        if ($key !== false && $this->getParamType() === Sql::PARAM_POSITIONAL)
            throw new \Exception('You cannot mixed positionnal and named parameter');
        if ($key === false && count($this->params) > 0 && $this->getParamType() === Sql::PARAM_NAMED)
            throw new \Exception('You cannot mixed positionnal and named parameter');

        // Add datas on params array
        if ($key) {
            $this->params[$key] = array(
                'parameter' => $parameter,
                'type' => $type
            );
        } else {
            $this->params[] = array(
                'parameter' => $parameter,
                'type' => $type
            );
        }
        return $this;
    }

    /**
     * Execute the query
     * @return Void
     */
    public function execute() {
        if (Sql::isDebug())
            $time = new Benchmark();

        if ($this->query === null || !$this->haveStatement())
            throw new \Exception('You must set a query before execute it...');

        // Free previous result stored
        $this->free();

        $paramToStm = array('');
        if ($this->getParamType() == Sql::PARAM_POSITIONAL) {
            foreach ($this->params as &$param) {
                // Set type on first line
                $paramToStm[0] .= $this->paramType[$param['type']];
                $paramToStm[] = &$param['parameter'];
            }
        } else {
            foreach ($this->namedParamOrder as &$param) {
                // Set type on first line
                $paramToStm[0] .= $this->paramType[$this->params[$param]['type']];
                $paramToStm[] = &$this->params[$param]['parameter'];
            }
        }

        // Call Mysqli bind_param method
        if (count($paramToStm) > 1)
            call_user_func_array(array($this->statement, 'bind_param'), $paramToStm);

        $this->statement->execute();

        // Storing results
        $this->statement->store_result();
        // Storing metaDatas

        $this->metaData = $this->statement->result_metadata();
        if ($this->metaData)
            $this->fields = $this->metaData->fetch_fields();


        if (Sql::isDebug()) {
            $time->stop();
            $sqlTime = $time->getInterval();
            $this->logs[] = array($this->query, $this->params, $sqlTime . 's', $this->getLastError());
        }
        if (Sql::isExplain() && \stripos($this->query, 'SELECT') !== false && \stripos($this->query, 'EXPLAIN') === false) {
            if (self::$explain === null)
                self::$explain = new self($this->configName);
            $explainSql = self::$explain;
            $explainSql->setQuery('EXPLAIN ' . $this->query);
            $explainSql->params = $this->params;
            $explainSql->execute();
            $explain = array();
            while ($datas = $explainSql->fetch(Sql::FETCH_ASSOC)) {
                if (count($explain) == 0)
                    $explain[] = \array_keys($datas);
                $explain[] = $datas;
            }
            $this->explains[] = $explain;
        }
        $this->params = array();
        return;
    }

    /**
     * Fetch result for query
     * @param Constant $type
     * @return Mixed
     */
    public function fetch($type = Sql::FETCH_OBJECT) {
        if (!$this->haveStatement() || $this->metaData === null)
            throw new \Exception('You must execute query before fetch result');

        if ($this->metaData === false)
            throw new \Exception('You can only fetch on select way query');

        switch ($type) {
            case Sql::FETCH_OBJECT:
                $fetch = false;
                $obj = new \stdClass();

                if (count($this->fields) >= 1) {
                    $fieldNames = array();
                    foreach ($this->fields as $field)
                        $fieldNames[] = &$obj->{$field->name};
                    call_user_func_array(array($this->statement, 'bind_result'), $fieldNames);
                    $fetch = $this->statement->fetch();
                }
                if ($fetch)
                    return $obj;
                break;
            case Sql::FETCH_ASSOC:
            case Sql::FETCH_ARRAY:
            case Sql::FETCH_ROW:
                $fetch = false;
                $array = array();
                if (count($this->fields) >= 1) {
                    $fieldNames = array();
                    foreach ($this->fields as $field)
                        $fieldNames[] = &$array[$field->name];
                    call_user_func_array(array($this->statement, 'bind_result'), $fieldNames);
                    $fetch = $this->statement->fetch();
                }

                // Formating array
                if (count($array) > 0) {
                    switch ($type) {
                        case Sql::FETCH_ARRAY:
                            $array = \array_merge($array, \array_values($array));
                            break;
                        case Sql::FETCH_ROW:
                            $array = \array_values($array);
                            break;
                    }
                }

                if ($fetch)
                    return $array;
                break;
            default:
                throw new \Exception('Type "' . $type . '" don\'t exists');
        }
        return false;
    }

    /**
     * Fetch all result of query
     * @param Constant $type
     * @return Mixed
     */
    public function fetchAll($type = Sql::FETCH_OBJECT) {
        $rows = array();
        while ($result = $this->fetch($type))
            $rows[] = $result;
        return $rows;
    }

    /**
     * See the number of results of query
     * @return Int
     */
    public function count() {
        if (!$this->haveStatement())
            throw new \Exception('You must execute query before see count result');
        return $this->statement->num_rows;
    }

    /**
     * Get the last id of INSERT query
     * @return Int
     */
    public function getLastId() {
        if (!$this->haveStatement())
            throw new \Exception('You must execute query before see lastId result');
        return $this->statement->insert_id;
    }

    /**
     * Get number of affected row with query
     * @return Int
     */
    public function affectedRows() {
        if (!$this->haveStatement())
            throw new \Exception('You must execute query before see affectedRows result');
        return $this->statement->affected_rows;
    }

    /**
     * Get the statement objet of query
     * @return Mysqli_STMT Can be null
     */
    public function getStatement() {
        return $this->statement;
    }

    /**
     * Free stored results of statement on memory
     * @return Void
     */
    public function free() {
        if ($this->metaData !== null && $this->metaData !== false) {
            $this->metaData->close();
            $this->metaData = null;
        }
        if ($this->haveStatement())
            $this->statement->free_result();
        return;
    }

    /**
     * Get status informations of server
     * @return String
     */
    public function status() {
        return $this->stat();
    }

    /**
     * Close server connexion
     * @return Boolean
     */
    public function disconnect() {
        // Free stored result, close statement
        $this->free();
        if ($this->haveStatement())
            $this->statement->close();
        $this->statement = null;

        // Close connexion
        if ($this->isConnected)
            $this->isConnected = !$this->close();
        return!$this->isConnected;
    }

    /**
     * Connect or reconnect server
     * @param Boolean $master Set if must be a slave or a master server
     * @return Void
     */
    public function connection($master = true) {
        // Get config
        if ($master)
            $server = SqlConfig::getInstance($this->configName)->getMasterServer();
        else
            $server = SqlConfig::getInstance($this->configName)->getSlaveServer();

        // Disconnect last server and reconnect on new configuration
        if ($server !== $this->serverConfiguration) {
            if ($this->isConnected)
                $this->disconnect();

            $this->serverConfiguration = $server;

            // Try to connect
            $this->isConnected = $this->real_connect(
                            $this->serverConfiguration->getHost(), $this->serverConfiguration->getLogin(), $this->serverConfiguration->getPassword(), $this->serverConfiguration->getDatabaseName(), $this->serverConfiguration->getPort()
            );
            // Set charset
            $this->set_charset(SqlConfig::getCharset());
            // Check if connexion success
            if (!$this->isConnected)
                throw new \Exception($this->error);
        }
        return;
    }

    /**
     * Detect if this is a read or writte query for get proper server
     * @param String $query
     * @return Boolean
     */
    protected function isReadQuery($query) {
        return stripos($query, 'select') !== false || stripos($query, 'show') !== false || stripos($query, 'describe') !== false;
    }

    /**
     * Check if it have statement
     * @return Boolean
     */
    public function haveStatement() {
        return ($this->statement !== null && $this->statement !== false);
    }

    /**
     * Close connection on destruct this object
     * @return Void
     */
    public function __destruct() {
        /*
          if(Sql::isDebug() && count($this->logs) > 1){
          Logger::getInstance()->addGroup('Mysqli', 'Mysqli debug logs',true);
          $totalTime = 0;
          foreach($this->logs as $k => &$log){
          if($k != 0)
          $totalTime += trim($log[2],'s');
          }
          Logger::getInstance()->table($this->logs, (count($this->logs) - 1).' queries in '.$totalTime.'s', 'Mysqli');
          // avoid multiple call
          $this->logs = array();
          }

          if(Sql::isExplain() && count($this->explains) > 1){
          Logger::getInstance()->addGroup('MysqliExplain', 'Mysqli explain logs',true);
          foreach($this->explains as &$explain){
          Logger::getInstance()->table($explain, 'Explain logs', 'MysqliExplain');
          }
          // avoid multiple call
          $this->explains = array();
          }
         */
        $this->disconnect();
    }

    /**
     * Get type of parameters on query
     * @return Constant Sql::PARAM_POSITIONAL || Sql::PARAM_NAMED
     */
    protected function getParamType() {
        return $this->bindParamType;
    }

    /**
     * Get metadatas for current query
     * @return Array
     */
    public function getMetaDatas() {
        return $this->fields;
    }

    /**
     * Return an object with last error and last error number
     * @return \stdClass $err->error && $err->numError
     */
    public function getLastError() {
        $error = null;
        if ($this->statement->error !== '') {
            $error = new \stdClass();
            $error->error = $this->statement->error;
            $error->numError = $this->statement->errno;
        }
        return $error;
    }

}
?>

